"""
@author: 江同学呀
@file: sort_search_results.py
@date: 2025/1/8 21:39
@desc:
    排序
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sort-search-results.html#_sort_values

    todo
"""
from typing import Union, Dict, Literal, List, Iterable, Optional

from espc.common.common import ASC, Order, Number, Unit
from espc.common.field_common import TimeFormat
from espc.common.query_common import DistanceType
from espc.common.sort_common import SortMode, NumericType
from espc.orm.model.base.base import _Base
from espc.orm.model.dsl.queries.base_queries import _BaseQueries
from espc.orm.model.mapping.field.base_field.base_field import _BaseField
from espc.orm.model.scripting.script import Script


class NestedSort(_Base):
    """
    嵌套排序
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sort-search-results.html#nested-sorting

    :param path:
        Defines on which nested object to sort. The actual sort field must be a direct field inside this nested object.
        When sorting by nested field, this field is mandatory.
        定义要排序的嵌套对象。实际的排序字段必须是此嵌套对象内的直接字段。按嵌套字段排序时，此字段为必填字段。
    :param filter_:
        A filter that the inner objects inside the nested path should match with in order for its field values to be
        taken into account by sorting. Common case is to repeat the query / filter inside the nested filter or query.
        By default no nested_filter is active.
        嵌套路径中的内部对象应与之匹配的过滤器，以便在排序时考虑其字段值。常见情况是在嵌套过滤器或查询中重复 query / filter。默认情况下，没有
        nested_filter处于活动状态。
    :param max_children:
        The maximum number of children to consider per root document when picking the sort value. Defaults to unlimited.
        选择排序值时，每个根文档要考虑的最大子项数。默认为 unlimited。
    :param nested:
        Same as top-level nested but applies to another nested path within the current nested object.
        与顶级嵌套相同，但适用于当前嵌套对象中的其他嵌套路径。
    :param :
    """
    def __init__(
            self, field: Union[_BaseField, str], path: Union[str, _BaseField], filter_: Iterable[_BaseQueries] = None,
            max_children: int = None, nested: "Sort" = None
    ):
        super().__init__()
        self._field: Union[_BaseField, str] = field
        self._path: Union[str, _BaseField] = path
        self._filter: Iterable[_BaseQueries] = filter_
        self._max_children: int = max_children
        self._nested: Optional[NestedSort] = nested
        return

    def filter(self, *filter_) -> "NestedSort":
        """
        过滤
        :param filter_:
        :return:
        """
        self._filter: Iterable[_BaseQueries] = filter_
        return self

    def _build(self) -> Dict:
        _body: Dict = {
            "path": self._path if isinstance(self._path, str) else self._path._field_name,
        }
        if self._filter:
            _body["filter"] = [_filter._build() for _filter in self._filter]
        if self._max_children is not None:
            _body["max_children"] = self._max_children
        if self._nested:
            _body["nested"] = self._nested._build()
        return {
            self._field if isinstance(self._field, str) else self._field._field_name: _body
        }


class GeoDistanceSort(_Base):
    """
    geo_distance 排序
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sort-search-results.html#geo-sorting

    :param distance_type:
        How to compute the distance. Can either be arc (default), or plane (faster, but inaccurate on long distances
        and close to the poles).
        如何计算距离。可以是 arc（默认）或 plane（更快，但在长距离和靠近极点时不准确）。
    :param mode:
        What to do in case a field has several geo points. By default, the shortest distance is taken into account when
        sorting in ascending order and the longest distance when sorting in descending order. Supported values are min,
        max, median and avg.
        如果字段具有多个地理点，该怎么办。默认情况下，按升序排序时考虑最短距离，按降序排序时考虑最长距离。支持的值为 min、max、median 和 avg。
    :param unit:
        The unit to use when computing sort values. The default is m (meters).
        计算排序值时使用的单位。默认值为 m（米）。
    :param ignore_unmapped:
        Indicates if the unmapped field should be treated as a missing value. Setting it to true is equivalent to
        specifying an unmapped_type in the field sort. The default is false (unmapped field cause the search to fail).
        指示是否应将未映射的字段视为缺失值。将其设置为 true 等效于在字段排序中指定unmapped_type。默认值为 false（未映射的字段会导致搜索失败）。
    :param order:
    :param value:
    """
    def __init__(
            self, field: Union[_BaseField, str],
            value: Union[List[Number], List[List[Number]], Dict[Literal["lat", "lon"], Number], str],
            distance_type: Union[DistanceType, Literal["arc", "plane"]] = None,
            order: Union[Order, Literal["asc", "desc"]] = ASC,
            unit: Union[Literal["m", "cm", "mm", "km", "in", "yd", "ft", "mi"], Unit] = None,
            mode: Union[Literal["min", "max", "sum", "avg", "median"], SortMode] = None, ignore_unmapped: bool = None
    ):
        super().__init__()
        self._field: Union[_BaseField, str] = field
        self._value: Union[List[Number], List[List[Number]], Dict[Literal["lat", "lon"], Number], str] = value
        self._distance_type: Union[DistanceType, Literal["arc", "plane"], None] = distance_type
        self._order: Union[Order, Literal["asc", "desc"]] = order
        self._unit: Union[Literal["m", "cm", "mm", "km", "in", "yd", "ft", "mi"], Unit, None] = unit
        self._mode: Union[Literal["min", "max", "sum", "avg", "median"], SortMode, None] = mode
        self._ignore_unmapped: bool = ignore_unmapped
        return

    def _build(self) -> Dict:
        _body: Dict = {
            self._field if isinstance(self._field, str) else self._field._field_name: self._value,
        }
        if self._distance_type:
            _body["distance_type"] = (
                self._distance_type if isinstance(self._distance_type, str) else self._distance_type.value
            )
        if self._order:
            _body["order"] = self._order if isinstance(self._order, str) else self._order.value
        if self._unit:
            _body["unit"] = (
                self._unit if isinstance(self._unit, str) else self._unit.value
            )
        if self._mode:
            _body["mode"] = self._mode if isinstance(self._mode, str) else self._mode.value
        if self._ignore_unmapped is not None:
            _body["ignore_unmapped"] = self._ignore_unmapped
        return {
            "_geo_distance": _body
        }


class ScriptBasedSort(_Base):
    """
    基于脚本的排序
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/sort-search-results.html#script-based-sorting

    :param type_:
    :param script:
    :param order:
    """

    def __init__(
            self, type_: str,
            script: Union[Script, Dict] = None,
            order: Union[Order, Literal["asc", "desc"]] = ASC
    ):
        super().__init__()
        self._type: str = type_
        self._script: Union[Script, Dict] = script
        self._order: Union[Order, Literal["asc", "desc"]] = order
        return

    def _build(self) -> Dict:
        _body: Dict = {
            "type": self._type,
            "order": self._order if isinstance(self._order, str) else self._order.value,
        }
        if self._script:
            _body["script"] = self._script._build()
        return {
            "_script": _body
        }


class Sort(_Base):
    """
    排序

    :param field: 字段
    :param order: 排序方式
        The order option can have the following values:
        asc             Sort in ascending order
        desc            Sort in descending order

        order 选项可以具有以下值：
        asc             按升序排序
        desc            按降序排序
    :param mode:
        Elasticsearch supports sorting by array or multi-valued fields. The mode option controls what array value is
        picked for sorting the document it belongs to. The mode option can have the following values:
        min             Pick the lowest value.
        max             Pick the highest value.
        sum             Use the sum of all values as sort value. Only applicable for number based array fields.
        avg             Use the average of all values as sort value. Only applicable for number based array fields.
        median          Use the median of all values as sort value. Only applicable for number based array fields.

        Elasticsearch 支持按数组或多值字段排序。mode 选项控制选择哪个数组值来对它所属的文档进行排序。mode 选项可以具有以下值：
        min             选择最低值。
        max             选择最高值。
        sum             使用所有值的总和作为排序值。仅适用于基于数字的数组字段。
        avg             使用所有值的平均值作为排序值。仅适用于基于数字的数组字段。
        median          使用所有值的中位数作为排序值。仅适用于基于数字的数组字段。
    :param numeric_type: 数值字段类型
        For numeric fields it is also possible to cast the values from one type to another using the numeric_type
        option. This option accepts the following values: ["double", "long", "date", "date_nanos"] and can be useful
        for searches across multiple data streams or indices where the sort field is mapped differently.
        对于数值字段，还可以使用 numeric_type 选项将值从一种类型强制转换为另一种类型。此选项接受以下值：
        [“double”， “long”， “date”， “date_nanos”] ，可用于跨多个数据流或索引进行搜索，其中排序字段的映射方式不同。
    :param nested: 嵌套
    :param format_: 日期格式


    """
    def __init__(
            self, field: Union[_BaseField], order: Union[Order, Literal["asc", "desc"]] = ASC,
            mode: Union[Literal["min", "max", "sum", "avg", "median"], SortMode] = None,
            numeric_type: Union[Literal["double", "long", "date", "date_nanos"], NumericType] = None,
            nested: NestedSort = None, format_: Union[str, TimeFormat] = None, missing: Union[Number, str] = None,
            unmapped_type: str = None,
            *args, **kwargs
    ):
        super().__init__(*args, **kwargs)
        self._field = field
        self._order: Union[Order, Literal["asc", "desc"]] = order
        self._mode: Union[Literal["min", "max", "sum", "avg", "median"], SortMode, None] = mode
        self._numeric_type: Union[Literal["double", "long", "date", "date_nanos"], NumericType, None] = numeric_type
        self._format: Union[str, TimeFormat] = format_
        self._missing: Union[Number, str] = missing
        self._unmapped_type: str = unmapped_type
        return

    def _build(self) -> Dict:
        if self._format or self:
            _body: Dict = {
                "order": self._order if isinstance(self._order, str) else self._order.value
            }
            if self._mode:
                _body["mode"] = self._mode if isinstance(self._mode, str) else self._mode.value
            if self._numeric_type:
                _body["numeric_type"] = (
                    self._numeric_type if isinstance(self._numeric_type, str) else self._numeric_type.value
                )

            if self._format:
                _body["format"] = self._format if isinstance(self._format, str) else self._format.value
        else:
            _body: str = self._order if isinstance(self._order, str) else self._order.value
        return {
            self._field._field_name: _body
        }

